今天看了一個人講解這個比賽,他也有提供notebok
雖然跟老師講的蠻不一樣的,但好像也可以試著理解
這個notebook 主要是沒有用Transformer裡面的trainer ,所以在train的時候會有比較多細微的步驟
我也有試一下用fastai 的learner,不過跑出不少bug,我看講師上課也是用transformer ,
在想說不定fastai 不適合做npl任務?
所以今天先看來這位網友的講解
import transformers
from transformers import DebertaV2TokenizerFast, DebertaV2ForSequenceClassification
可以看到還是導入了transformers
不過這邊的Tokenizer是用DebertaV2,這個在kaggle notebook的add data 的時候要記得加載,這樣之後pretrained 的時候要指定路徑才不會出錯
import torch
from torch import optim
from torch.utils.data import DataLoader, Dataset, random_split
from torchmetrics.regression import PearsonCorrCoef
到這邊都很一般,不一樣的地方是這邊加載了torch 的optim ,優化器
import numpy as np
import random
import timeit
from tqdm import tqdm
tqdm 就只是show 進度條的工具
前一天有提到,我們的conext 要去合併另一個數據集,這樣才會有title
所以這位網友也做了一樣的事,只是合併的方法不同
updated_train_df = train_df.merge(patents_df, left_on='context', right_on='code')
然後重點是他把anchor(第一個詞)跟第二個資料集查到的title 合併成一個欄位,叫做input
updated_train_df = train_df.merge(patents_df, left_on='context', right_on='code')
updated_train_df["input"] = updated_train_df["title"] + " " + updated_train_df["anchor"]
updated_train_df.tail()
然後處理資料,這邊其實就是做資料處理,然後做分詞與詞向量,其實如果看不懂的話,也可以一步一步慢慢寫
網友自定義了一個類別,寫得比較彈性化
class PhraseTrainDataset(Dataset):
def __init__(self, inputs, targets, scores, tokenizer):
self.scores = scores
self.encodings = tokenizer(inputs, targets, padding=True, truncation=True, max_length=MAX_LENGTH)
def __getitem__(self, idx):
out_dic = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}
out_dic["scores"] = self.scores[idx]
return out_dic
def __len__(self):
return len(self.scores)
然後把train的資料合併好,也做好詞向量的轉化後,就要切訓練集與驗證集
這邊也是沒有做交叉驗證,直接random
train_dataset, val_dataset = random_split(dataset, [0.9, 0.1], generator=generator)
下面就開始創建dataloader 了
train_dataloader = DataLoader(dataset=train_dataset,
batch_size=BATCH_SIZE,
shuffle=True)
optimizer = optim.AdamW(model.parameters(), lr=LEARNING_RATE)
pearson = PearsonCorrCoef()
start = timeit.default_timer()
for epoch in tqdm(range(EPOCHS), position=0, leave=True):
model.train()
train_running_loss = 0 #初始化一個變數train_running_loss,用於計算訓練過程中的損失。
for idx, sample in enumerate(tqdm(train_dataloader, position=0, leave=True)):
input_ids = sample['input_ids'].to(device) #從數據批次中提取input_ids,並將其移動到GPU或CPU上
attention_mask = sample['attention_mask'].to(device)
targets = sample["scores"].to(device)
outputs = model(input_ids=input_ids, attention_mask=attention_mask, labels=targets) #通過模型前向傳播,計算模型的輸出,並根據目標值計算損失。
loss = outputs.loss #提取從模型輸出中計算的損失。
optimizer.zero_grad() #清零優化器的梯度,以準備進行反向傳播。
loss.backward() #計算損失相對於模型參數的梯度,以便進行梯度下降。
optimizer.step() #根據計算的梯度更新模型的參數。
train_running_loss += loss.item() #將當前批次的損失值添加到train_running_loss中,用於計算平均訓練損失。
train_loss = train_running_loss / (idx + 1) #計算平均訓練損失,將它設為當前訓練週期的訓練損失。
model.eval() #設置模型為評估模式,這將關閉Dropout等訓練時特定的操作。
val_running_loss = 0
#初始化兩個list,用於存儲模型預測的分數(preds)和真實分數(golds)。
preds = []
golds = []
with torch.no_grad():
for idx, sample in enumerate(tqdm(val_dataloader, position=0, leave=True)):
input_ids = sample['input_ids'].to(device)
attention_mask = sample['attention_mask'].to(device)
targets = sample["scores"].to(device)
outputs = model(input_ids=input_ids, attention_mask=attention_mask, labels=targets)
preds.extend([float(i) for i in outputs["logits"].squeeze()])
golds.extend([float(i) for i in targets])
val_running_loss += outputs.loss.item()
val_loss = val_running_loss / (idx + 1)
print("-"*30)
print(f"Pearson Score: {float(pearson(torch.tensor(preds), torch.tensor(golds))):.4f}")
print(f"Train Loss EPOCH {epoch+1}: {train_loss:.4f}")
print(f"Valid Loss EPOCH {epoch+1}: {val_loss:.4f}")
print("-"*30)
stop = timeit.default_timer()
print(f"Training Time: {stop-start:.2f}s")
這邊就比較多,也是今天主要要看的地方。
其中shuffle 是打亂的意思
optimizer = optim.AdamW(model.parameters(), lr=LEARNING_RATE)
pearson = PearsonCorrCoef()
這邊用到一個優化器adamW,他是一個優化權重以降低loss 的優化器,我們之前上課用的是SGD,那他詳細的理論,可能要去讀paper ,這邊先知道他是另一種優化器即可。
然後進到他的訓練loop 來看一下
訓練循環:這個部分包含了模型的訓練過程。它會迭代EPOCHS次,每個迭代都代表一個訓練周期。
模型訓練模式:首先,將模型設置為訓練模式(model.train())。這是因為在訓練過程中,模型需要計算梯度以便進行權重更新。
訓練數據的迭代:然後,對訓練數據進行迭代,其中每個樣本由train_dataloader生成。這裡使用tqdm來顯示進度條,使得能夠實時查看訓練的進度。
計算損失和反向傳播:對於每個樣本,首先將其移動到設備(GPU或CPU)上。然後使用模型進行預測(outputs = model(...))並計算損失(loss)。接著,通過optimizer.zero_grad()清除之前的梯度,使用loss.backward()計算梯度,最後使用optimizer.step()來更新模型的權重。
訓練損失計算:在每個訓練循環的結尾,計算並記錄訓練損失(train_loss)。
其中有一個atten_mask,查了一下解釋如下:
attention_mask 是用於 Transformer 模型的一種重要輸入。它是一個二進制掩碼(binary mask),用於告訴模型在輸入序列中哪些位置需要注意,哪些位置應該被忽略。這個掩碼通常用於處理可變長度的序列,以確保模型不會在填充的位置上產生不必要的注意力。
具體來說:
attention_mask 是一個與輸入序列長度相同的二進制張量,其中包含兩個值:0 和 1。
對於輸入序列中的每個位置,如果該位置是有效的(包含有意義的詞或標記),則對應的 attention_mask 值為 1;如果該位置是填充的(無意義的填充標記),則 attention_mask 值為 0。
為什麽需要這個掩碼?
Transformer 模型的注意力機制會根據輸入序列和位置計算權重,然後在編碼器和解碼器之間傳遞這些權重。如果輸入序列有填充,沒有 attention_mask 的情況下,模型可能會關注到填充位置,從而浪費計算資源並可能導致模型性能下降。
通過使用 attention_mask,我們可以告訴模型只關注有效位置,忽略填充位置。
在這段代碼中,sample['attention_mask'] 從數據集中的樣本中提取了 attention_mask。然後,通過 .to(device) 將其移動到了可以加速計算的設備(通常是 GPU)上,以便後續的模型前向傳播可以在 GPU 上高效執行。
attention_mask 並不是模型自動生成的,而是在數據預處理階段生成的。在文本數據輸入到模型之前,通常會進行文本編碼和填充處理,並且根據文本長度生成相應的 attention_mask。這是為了確保模型在處理文本時能夠正確地注意到有意義的部分,而忽略填充的部分。
所以在做tokenizer 的時候,就自動加入這個欄位
對比一下之前講師使用的Trainer
from transformers import TrainingArguments,Trainer
bs = 128
epochs = 4
lr = 8e-5
args = TrainingArguments('outputs', learning_rate=lr, warmup_ratio=0.1, lr_scheduler_type='cosine', fp16=True,
evaluation_strategy="epoch", per_device_train_batch_size=bs, per_device_eval_batch_size=bs*2,
num_train_epochs=epochs, weight_decay=0.01, report_to='none')
model = AutoModelForSequenceClassification.from_pretrained(model_nm, num_labels=1)
trainer = Trainer(model, args, train_dataset=dds['train'], eval_dataset=dds['test'],
tokenizer=tokz, compute_metrics=corr_d)
trainer.train();
這之中並沒有指定要使用什麼優化器,但其實預設就是使用adamW ,這個設定中,還有指定lr_scheduler_type。
所以今天這個網友,其實沒有多做什麼事情,就只是把兩個資料合併,用不一樣的pretrained 模型 ,本質上並沒有什麼不同,但是可以幫我們更了解到trainer 裡面在做什麼